Skip to content

Add durable cancellation mode for useAgentChat#1484

Merged
threepointone merged 3 commits into
mainfrom
durable-chat-cancellation-policy
May 11, 2026
Merged

Add durable cancellation mode for useAgentChat#1484
threepointone merged 3 commits into
mainfrom
durable-chat-cancellation-policy

Conversation

@whoiskatrin
Copy link
Copy Markdown
Contributor

@whoiskatrin whoiskatrin commented May 8, 2026

Summary

Changes useAgentChat cancellation semantics so browser/client stream cleanup is local-only by default:

useAgentChat({
  agent
});

By default, the browser is treated as a reconnectable observer of a server turn. Generic client-side stream abort/cleanup does not send cf_agent_chat_request_cancel, so the server turn can continue and be resumed later. Explicit stop() still sends cf_agent_chat_request_cancel and cancels the server turn.

For apps that intentionally want request-lifetime behavior, this adds:

cancelOnClientAbort?: boolean;

Set cancelOnClientAbort: true to make generic client abort/stream cleanup cancel the server turn.

What the issue actually was

This is not about a raw WebSocket close directly calling server-side abort.

The problematic path is one layer higher:

  1. useAgentChat wraps AI SDK useChat.
  2. AI SDK useChat owns a local active response with an AbortController.
  3. WebSocketChatTransport.sendMessages() receives that signal as options.abortSignal, and also exposes a ReadableStream whose cancel() method can be called by the client stream lifecycle.
  4. Before this PR, both paths used the same onAbort() handler.
  5. That handler always sent cf_agent_chat_request_cancel.
  6. The server correctly treated that frame as real server-turn cancellation.

So the server could not distinguish these two cases:

  • the user/app explicitly clicked Stop and wanted to cancel the server turn
  • the client-side response/stream was aborted or disposed as part of browser/React/AI SDK lifecycle

For Durable Object backed agents, the second case should often be recoverable via stream resumption. The browser can disappear or detach while the durable server turn continues and buffers chunks for a later reconnect.

What changed

  • Added cancelOnClientAbort?: boolean to useAgentChat and WebSocketChatTransport.
    • Default is false: generic client abort/stream cancel is local-only.
    • true opts into request-lifetime behavior where generic client abort cancels the server turn.
  • Removed the earlier durable / serverTurnCancellation API shape in favor of the clearer boolean toggle.
  • Kept explicit useAgentChat().stop() as server-side cancellation regardless of cancelOnClientAbort.
  • Updated docs and README for the default resumable semantics and the opt-in request-lifetime mode.

Follow-up changes from review

  • Separated "active server turn is cancellable" from "local stream is attached" inside WebSocketChatTransport.
  • Kept detached server turns explicitly cancellable after local-only client abort.
  • Made explicit cancellation work for resumed streams created by reconnectToStream().
  • Added support for explicitly cancelling fallback-observed server turns via observeServerTurn().
  • Cleared stored cancellation state when the server later sends done for a detached turn.
  • Avoided starting a new server turn when sendMessages() is called with an already-aborted signal.
  • Added edge-case coverage for local-only abort, explicit stop after detach, resumed-stream stop, fallback-observed stop, and cancelOnClientAbort: true with both abort signals and stream.cancel().

Tests

  • npm run test:workers -w @cloudflare/ai-chat -- ws-transport-abort.test.ts ws-transport-cancellation-policy.test.ts ws-transport-resume.test.ts
  • npm run test:react -w @cloudflare/ai-chat -- use-agent-chat.test.tsx
  • npm run check

Related

Fixes #1471

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 8, 2026

🦋 Changeset detected

Latest commit: df0326b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/ai-chat Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 8, 2026

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1484

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1484

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1484

hono-agents

npm i https://pkg.pr.new/hono-agents@1484

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1484

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1484

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1484

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1484

commit: df0326b

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@advait
Copy link
Copy Markdown

advait commented May 8, 2026

@whoiskatrin LGTM thank you!

@threepointone
Copy link
Copy Markdown
Contributor

Very nice. I like this PR, but I think it could go further. This new toggle should probably be the default behaviour, since it's the overwhelming actual desired usecase (navigating to a new chat session shouldn't kill an existing stream), let's also rename the flag to cancelOnClientAbort, and default it to false. Pushed up a commit with these changes.

@threepointone threepointone force-pushed the durable-chat-cancellation-policy branch from d91facd to 26d433b Compare May 11, 2026 20:01
Co-authored-by: Cursor <cursoragent@cursor.com>
@threepointone threepointone force-pushed the durable-chat-cancellation-policy branch from 26d433b to d4f226a Compare May 11, 2026 20:17
Co-authored-by: Cursor <cursoragent@cursor.com>
@threepointone threepointone merged commit 364a45d into main May 11, 2026
4 checks passed
@threepointone threepointone deleted the durable-chat-cancellation-policy branch May 11, 2026 21:27
@github-actions github-actions Bot mentioned this pull request May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useAgentChat should not transparently cancel durable turns on client stream disposal

3 participants